/*
	Copyright 2008, 2009, 2010 Brightworks, Inc.
	
	This file is part of Language Mentor.
	
	Language Mentor is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	Language Mentor is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with Language Mentor.  If not, see <http://www.gnu.org/licenses/>.
*/
/*
    This class is responsible for storing, indexing and safeguarding data in such
    a way that it always reflects the data stored in the SQLite DB.

    'Primary Storage Lists' are DataPrimaryStorageList instances which store data. They are only modified by:
        - loadDataFromDB()
        - insertData()
        - updateData()
        - deleteData()

    Clients can access data by:
        - Calling getter methods that return indexes
        - selectData()

    This code assumes that if any SQLite operation fails we'll re-init this object's data
    from the DB. This may mean that we'll restart the app - I haven't thought this through
    yet. So, for example, we add INSERT data to our primary storage lists before sending it
    into the DB, knowing that this won't create a problem because the storage list data is
    very temporary data, and won't survive if there's a DB problem. I'm taking
    this approach because it allows me to avoid a lot of complex failure-recovery code

*/
package com.langcollab.languagementor.model
{
    import com.brightworks.db.ISQLiteOperation;
    import com.brightworks.db.SQLiteQueryData;
    import com.brightworks.db.SQLiteQueryData_Insert;
    import com.brightworks.db.SQLiteQueryData_Select;
    import com.brightworks.db.SQLiteTransaction;
    import com.brightworks.db.SequentialSQLiteTransactionSet;
    import com.brightworks.error.BwErrorEvent;
    import com.brightworks.util.Debug;
    import com.brightworks.util.Utils_Object;
    import com.brightworks.vo.VO;
    import com.langcollab.languagementor.constant.Constant_Misc;
    import com.langcollab.languagementor.vo.*;
    
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.filesystem.File;
    import flash.utils.Dictionary;
    
    import mx.collections.ArrayCollection;


    public class Data extends EventDispatcher {

        // --------------------------------------------
        //
        //           Primary Storage Lists
        //
        // --------------------------------------------

        private var _chunkList                         :ArrayCollection;
        private var _chunkFileList                     :ArrayCollection;
        private var _chunkLearningFeedbackList         :ArrayCollection;
        private var _contentProviderAccountList        :ArrayCollection;
        private var _languageList                      :ArrayCollection;
        private var _learningFeedbackList              :ArrayCollection;
        private var _learningModeList                  :ArrayCollection;
        private var _levelList                         :ArrayCollection;
        private var _moduleVersionList                 :ArrayCollection;
        private var _moduleVersionConsumptionList      :ArrayCollection;
        private var _moduleVersionContributionList     :ArrayCollection;
        private var _moduleVersionNativeLanguageList   :ArrayCollection;
        private var _moduleVersionQualityFeedbackList  :ArrayCollection;
        private var _moduleVersionTagList              :ArrayCollection;
        private var _moduleVersionTargetLanguageList   :ArrayCollection;
        private var _qualityFeedbackList               :ArrayCollection;
        private var _userList                          :ArrayCollection;

        // props are the names of this class's "primary storage lists" (above) and values are the class for the type of VO that each stores
        private var _index_VOClass_by_primaryStorageListName:Object =
                {
                    _chunkList                         :ChunkVO,
                    _chunkFileList                     :ChunkFileVO,
                    _chunkLearningFeedbackList         :ChunkLearningFeedbackVO,
                    _contentProviderAccountList        :ContentProviderAccountVO,
                    _languageList                      :LanguageVO,
                    _learningFeedbackList              :LearningFeedbackVO,
                    _learningModeList                  :LearningModeVO,
                    _levelList                         :LevelVO,
                    _moduleVersionList                 :ModuleVersionVO,
                    _moduleVersionConsumptionList      :ModuleVersionConsumptionVO,
                    _moduleVersionContributionList     :ModuleVersionContributionVO,
                    _moduleVersionNativeLanguageList   :ModuleVersionNativeLanguageVO,
                    _moduleVersionQualityFeedbackList  :ModuleVersionQualityFeedbackVO,
                    _moduleVersionTagList              :ModuleVersionTagVO,
                    _moduleVersionTargetLanguageList   :ModuleVersionTargetLanguageVO,
                    _qualityFeedbackList               :QualityFeedbackVO,
                    _userList                          :UserVO
                }

        // --------------------------------------------
        //
        //           Public Indexes
        //
        // --------------------------------------------

        private var _index_chunkVOs_by_moduleVersionVO:Dictionary;
        public function get index_chunkVOs_by_moduleVersionVO():Dictionary {
        	return _index_chunkVOs_by_moduleVersionVO;
        }

        private var _index_chunkFileVOs_by_chunkVO:Dictionary;
        public function get index_chunkFileVOs_by_chunkVO():Dictionary {
        	return _index_chunkFileVOs_by_chunkVO;
        }

        private var _index_chunkFileVOs_by_chunkVO_plus_languageId:Dictionary;
        public function get index_chunkFileVOs_by_chunkVO_plus_languageId():Dictionary {
        	return _index_chunkFileVOs_by_chunkVO_plus_languageId;
        }

        private var _index_contentProviderDisplayName_by_contentProviderId:Object;
        public function get index_contentProviderDisplayName_by_contentProviderId():Object {
        	return _index_contentProviderDisplayName_by_contentProviderId;
        }

        private var _index_language_id_by_iso639_3Code:Dictionary;
        public function get index_language_id_by_iso639_3Code():Dictionary {
        	return _index_language_id_by_iso639_3Code;
        }

        private var _index_languageVO_by_id:Dictionary;
        public function get index_languageVO_by_id():Dictionary {
            return _index_languageVO_by_id;
        }

        private var _index_learningFeedback_id_by_labelToken:Dictionary;
        public function get index_learningFeedback_id_by_labelToken():Dictionary {
            return _index_learningFeedback_id_by_labelToken;
        }

        private var _index_learningFeedbackVO_by_id:Dictionary;
        public function get index_learningFeedbackVO_by_id():Dictionary {
            return _index_learningFeedbackVO_by_id;
        }

        private var _index_learningMode_id_by_locationInOrder:Dictionary;
        public function get index_learningMode_id_by_locationInOrder():Dictionary {
            return _index_learningMode_id_by_locationInOrder;
        }

        private var _index_learningModeVO_by_id:Dictionary;
        public function get index_learningModeVO_by_id():Dictionary {
            return _index_learningModeVO_by_id;
        }

        private var _index_level_locationInOrder_by_id:Dictionary;
        public function get index_level_locationInOrder_by_id():Dictionary {
            return _index_level_locationInOrder_by_id;
        }

        private var _index_levelVO_by_id:Dictionary;
        public function get index_levelVO_by_id():Dictionary {
            return _index_levelVO_by_id;
        }

        private var _index_moduleVersionVO_by_chunkVO:Dictionary;
        public function get index_moduleVersionVO_by_chunkVO():Dictionary {
            return _index_moduleVersionVO_by_chunkVO;
        }

        private var _index_moduleVersionNativeLanguageVOs_by_moduleVersionVO:Dictionary;
        public function get index_moduleVersionNativeLanguageVOs_by_moduleVersionVO():Dictionary {
            return _index_moduleVersionNativeLanguageVOs_by_moduleVersionVO;
        }

        private var _index_moduleVersionNativeLanguageVOs_by_moduleVersionVO_plus_languageId:Dictionary;
        public function get index_moduleVersionNativeLanguageVOs_by_moduleVersionVO_plus_languageId():Dictionary {
            return _index_moduleVersionNativeLanguageVOs_by_moduleVersionVO_plus_languageId;
        }

        private var _index_moduleVersionTargetLanguageVOs_by_moduleVersionVO:Dictionary;
        public function get index_moduleVersionTargetLanguageVOs_by_moduleVersionVO():Dictionary {
            return _index_moduleVersionTargetLanguageVOs_by_moduleVersionVO;
        }

        private var _index_moduleVersionTargetLanguageVOs_by_moduleVersionVO_plus_languageId:Dictionary;
        public function get index_moduleVersionTargetLanguageVOs_by_moduleVersionVO_plus_languageId():Dictionary {
            return _index_moduleVersionTargetLanguageVOs_by_moduleVersionVO_plus_languageId;
        }

        private var _index_qualityFeedbackVO_by_id:Dictionary;
        public function get index_qualityFeedbackVO_by_id():Dictionary {
            return _index_qualityFeedbackVO_by_id;
        }

        // --------------------------------------------
        //
        //           Other Properties
        //
        // --------------------------------------------

        private var _callbacks:Callbacks;
        private var _currentSQLiteOperation:ISQLiteOperation;
		private var _databaseFile:File;
        private var _index_primaryStorageListName_by_VOClass:Dictionary
        private var _index_primaryStorageListName_by_queryData:Dictionary;   // Props are SQLiteQueryData instances, values are the names of the primary storage lists for which they are retrieving data


        // --------------------------------------------
        //
        //
        //
        //           Public Methods
        //
        //
        //
        // --------------------------------------------

        public function Data(){}

		/*public function deleteData(deleteCriteriaVO:VO, callbacks:Callbacks):void {
		
		}*/
		
		public function init(callbacks:Callbacks):void
		{
			Debug.info("Data.init()");
			_callbacks = callbacks;
			_databaseFile = File.applicationStorageDirectory.resolvePath(
				Constant_Misc.FILEPATHINFO_DB_FOLDER_NAME +
				File.separator +
				Constant_Misc.FILEPATHINFO_DB_FILE_FILE_NAME   );
			_index_primaryStorageListName_by_VOClass    = new Dictionary();
			_index_primaryStorageListName_by_queryData  = new Dictionary();
			var primaryStorageListName:String;
			var voClass:Class;
			var voInstance:VO;
			var queryDataInstance:SQLiteQueryData_Select;
			var queryDataList:Array = [];
			for (primaryStorageListName in _index_VOClass_by_primaryStorageListName) {
				queryDataInstance = new SQLiteQueryData_Select();
				voClass = _index_VOClass_by_primaryStorageListName[primaryStorageListName];
				voInstance = new voClass();
				queryDataInstance.vo = voInstance;
				queryDataList.push(queryDataInstance);
				_index_primaryStorageListName_by_VOClass[voClass]             = primaryStorageListName;
				_index_primaryStorageListName_by_queryData[queryDataInstance] = primaryStorageListName;
			}
			var op:SequentialSQLiteTransactionSet = new SequentialSQLiteTransactionSet(queryDataList);
			op.addEventListener(Event.COMPLETE, onInitPrimaryStorageListsFromSQLiteDBOperationComplete);
			op.addEventListener(BwErrorEvent.ERROR_GENERIC, onSQLiteDBOperationError);
			_currentSQLiteOperation = op;
			op.execute(_databaseFile);
		}
		
		public function insertData(queryDataList:Array, callbacks:Callbacks):void
		{
			Debug.info("Data.insertData()");
			if (_currentSQLiteOperation != null) 
				Debug.error_Fatal(["Data.insertData(): _currentSQLiteOperation != null", "_currentSQLiteOperation:", _currentSQLiteOperation, "queryDataList arg:", queryDataList]);
			_callbacks = callbacks;
			insertVOsIntoPrimaryStorageListsAndSetTheirIds(queryDataList);
			initIndexes();
			var transaction:SQLiteTransaction = new SQLiteTransaction(queryDataList);
			_currentSQLiteOperation = transaction;
			transaction.addEventListener(Event.COMPLETE,              onSQLiteDBDataModificationOperationResult);
			transaction.addEventListener(BwErrorEvent.ERROR_GENERIC,  onSQLiteDBOperationError);
			transaction.execute(_databaseFile);
		}
			
		public function selectData(selectionCriteriaVO:VO):Array {
			// This only allows 'one table' queries
			var result:Array = [];
			var selectionPropNameList:Array = selectionCriteriaVO.getPropNameList_SetProps()
			var storageList:ArrayCollection = getStorageListBasedOnVO(selectionCriteriaVO);
			var propName:String;
			var vo:VO;
			var matches:Boolean;
			for each (vo in storageList) {
				matches = true;
				for each (propName in selectionPropNameList) {
					if (vo[propName] != selectionCriteriaVO[propName]) {
						matches = false;
						break;
					}
				}
				if (matches) {
					result.push(vo)
				}
			}
			return result;
		}
		
		/*public function updateData(queryDataList:Array, callbacks:Callbacks):void {
		
		}*/
		
        // --------------------------------------------
        //
        //           Private Methods
        //
        // --------------------------------------------

        private function createIndex_AddDimensions(passedInIndex:Dictionary, propertyNameList:Array):Dictionary {
        	// This method adds one or more dimensions to an existing index whose values consist
        	// of arrays of VOs. If the propertyNameList argument contains more than one item
        	// this happens through a recursive process.
        	//
        	// Example:
        	//
        	//  In initial (non-yet-recursive) call, the following are passed in:
        	//      1. passedInIndex: A Dictionary whose properties are ModuleVersionVO instances, and whose values
        	//         are arrays of ModuleVersionNativeLanguageVO instances
			//      2. propertyNameList: ["languageId", "allChunksHaveAudioFile"]
			//
			//  The Resulting index would be named like this:
			//		_index_moduleVersionNativeLanguageVOs_by_moduleVersionVO_plus_languageId_plus_allChunksHaveAudioFile
			//
			//	And would look like this:
			//		{
			//			aModuleVersionVOInstance :
			//				{
			//					1 :                             - a languageId value
			//						{
			//							true :					- a allChunksHaveAudioFile value
			//								[
			//									aModuleVersionNativeLanguageVOInstance,
			//									aModuleVersionNativeLanguageVOInstance
			//								],
			//							false :
			//								[],					- none of the native languages for this
			//													  module version are incomplete
			//
			//  And would be accessed like this:
			//		_index_..._plus_allChunksHaveAudioFile[aModuleVersionVOInstance][1][true]
			//
			//  This would retrieve an array of all the ModuleVersionNativeLanguageVO instances
			//  	that meet these criteria:
			//			1. Are associated with the specified ModuleVersionVO instance
			//          2. Their languageId property is set to 1
			//			3. Their allChunksHaveAudioFile property is set to true

			var result:Dictionary = new Dictionary();
        	var currentPropValForVOToBeSorted:Object;
        	var newLevelSortingList:Dictionary;
        	var passedInIndexPropKey:Object;
        	var passedInIndexPropValList:Array; // This contains the VOs that will be sorted
        	var voToBeSorted:VO;
        	var currentPropertyName:String = propertyNameList.shift();
        	for (passedInIndexPropKey in passedInIndex) {
        		newLevelSortingList = new Dictionary();
				passedInIndexPropValList = passedInIndex[passedInIndexPropKey];
				for each (voToBeSorted in passedInIndexPropValList) {
					currentPropValForVOToBeSorted = voToBeSorted[currentPropertyName]
					if (!newLevelSortingList.hasOwnProperty(currentPropValForVOToBeSorted)) {
						newLevelSortingList[currentPropValForVOToBeSorted] = [];
					}
					(newLevelSortingList[currentPropValForVOToBeSorted] as Array).push(voToBeSorted);
				}
				if (propertyNameList.length > 0) {
					// Recursive call
					newLevelSortingList = createIndex_AddDimensions(newLevelSortingList, Utils_Object.cloneInstance(propertyNameList));
				}
				// Replace the Array that was passed in for this passedInIndexPropKey with Dictionary containing the
				// same VOs, but sorted by the values contained in the property specified by currentPropertyName. Also,
				// if this was recursive, that has already happened, so said Dictionary's properties have also been sorted.
				result[passedInIndexPropKey] = newLevelSortingList;
        	}
			return result;
        }

        private function createIndex_ReferencedVO_by_ReferencingVO(referencedVOClass:Class, referencingVOClass:Class):Dictionary {
        	var result:Dictionary = new Dictionary();
        	var referencedVOListName:String    = _index_primaryStorageListName_by_VOClass[referencedVOClass];
        	var referencingVOListName:String   = _index_primaryStorageListName_by_VOClass[referencingVOClass];
        	var referencedVOList:ArrayCollection     = this[referencedVOListName];
        	var referencingVOList:ArrayCollection    = this[referencingVOListName];
        	var referencingVO:VO;
        	var referencedVOCandidate:VO;
        	var bMatchFound:Boolean;
        	for each (referencingVO in referencingVOList) {
        		bMatchFound = false;
        		for each (referencedVOCandidate in referencedVOList) {
        			if (referencedVOCandidate.isReferencingInstance(referencingVO)) {
        				if (bMatchFound) 
							Debug.error_Fatal(["Data.createIndex_ReferencedVO_by_ReferencingVO(): referencing VO references more than one VO", "Referencing VO:", referencingVO, "Referenced VO List:", referencedVOList]);
        				bMatchFound = true;
        				result[referencedVOCandidate] = referencingVO;
        			}
        		}
        	}
			return result;
        }

        private function createIndex_ReferencingVOs_by_ReferencedVO(referencingVOClass:Class, referencedVOClass:Class):Dictionary {
        	var result:Dictionary = new Dictionary();
        	var referencedVOListName:String            = _index_primaryStorageListName_by_VOClass[referencedVOClass];
        	var referencingVOCandidateListName:String  = _index_primaryStorageListName_by_VOClass[referencingVOClass];
        	var referencedVOList:ArrayCollection            = this[referencedVOListName];
        	var referencingVOCandidateList:ArrayCollection  = this[referencingVOCandidateListName];
        	var referencedVO:VO;
        	var referencingVOCandidate:VO;
        	var resultSublist:Array;
        	for each (referencedVO in referencedVOList) {
        		resultSublist = [];
        		result[referencedVO] = resultSublist;
        		for each (referencingVOCandidate in referencingVOCandidateList) {
        			if (referencedVO.isReferencingInstance(referencingVOCandidate)) {
        				resultSublist.push(referencingVOCandidate);
        			}
        		}
        	}
			return result;
        }

        private function createIndex_VOField_by_SecondVOField(storageList:ArrayCollection, valuePropName:String, keyPropName:String):Dictionary
        {
            var result:Dictionary = new Dictionary();
            var vo:Object = null;
            for (var i:int = 0; i < storageList.length; i++)
            {
                vo = storageList[i];
                try
                {
                    result[vo[keyPropName]] = vo[valuePropName];
                }
                catch (e:ReferenceError)
                {
					Debug.error_Fatal("Data.createIndex_VOField: " + e.message);
                }
            }
            return result;
        }

        private function createIndex_VO_by_id(storageList:ArrayCollection):Dictionary
        {
            var result:Dictionary = new Dictionary();
            var vo:Object = null;
            for (var i:int = 0; i < storageList.length; i++)
            {
                vo = storageList[i];
                try
                {
                    result[vo.id] = vo;
                }
                catch (e:ReferenceError)
                {
					Debug.error_Fatal("Data.createIndex_VO_by_id: " + e.message);
                }
            }
            return result;
        }

        private function getStorageListBasedOnVO(vo:VO):ArrayCollection {
            var s:String = Utils_Object.getClassNameForInstance(vo);
            s = s.substring(0,1).toLowerCase() + s.substring(1, s.length - 2);
            s = "_" + s + "List";
            return ArrayCollection(this[s]);
        }

        private function initIndexes():void
        {
        	Debug.info("Data.initIndexes()");
            // Indexes that find VOs by their  integer ID field
            _index_languageVO_by_id                        = createIndex_VO_by_id(_languageList);
            _index_learningFeedbackVO_by_id                = createIndex_VO_by_id(_learningFeedbackList);
            _index_learningModeVO_by_id                    = createIndex_VO_by_id(_learningModeList);
            _index_levelVO_by_id                           = createIndex_VO_by_id(_levelList);
            _index_qualityFeedbackVO_by_id                 = createIndex_VO_by_id(_qualityFeedbackList);

            // Indexes that find a VO field value by another VO field value
            _index_contentProviderDisplayName_by_contentProviderId   = createIndex_VOField_by_SecondVOField(_contentProviderAccountList,  "contentProviderDisplayName",  "contentProviderId"  );
            _index_language_id_by_iso639_3Code                       = createIndex_VOField_by_SecondVOField(_languageList,                "id",                          "iso639_3Code"       );
            _index_learningMode_id_by_locationInOrder                = createIndex_VOField_by_SecondVOField(_learningModeList,            "id",                          "locationInOrder"    );
            _index_level_locationInOrder_by_id                       = createIndex_VOField_by_SecondVOField(_levelList,                   "locationInOrder",             "id"                 );
            _index_learningFeedback_id_by_labelToken                 = createIndex_VOField_by_SecondVOField(_learningFeedbackList,        "id",                          "labelToken"         );

			// Indexes that find a referenced VO by a referencing VO
			_index_moduleVersionVO_by_chunkVO = createIndex_ReferencedVO_by_ReferencingVO(ModuleVersionVO, ChunkVO  );

        	// Indexes that find Arrays of referencing VOs based on referenced VO
        	_index_chunkVOs_by_moduleVersionVO =
        		                     createIndex_ReferencingVOs_by_ReferencedVO(ChunkVO, ModuleVersionVO);
        	_index_chunkFileVOs_by_chunkVO =
        		                     createIndex_ReferencingVOs_by_ReferencedVO(ChunkFileVO, ChunkVO);
        	_index_moduleVersionNativeLanguageVOs_by_moduleVersionVO =
        		                     createIndex_ReferencingVOs_by_ReferencedVO(ModuleVersionNativeLanguageVO, ModuleVersionVO);
        	_index_moduleVersionTargetLanguageVOs_by_moduleVersionVO =
        		                     createIndex_ReferencingVOs_by_ReferencedVO(ModuleVersionTargetLanguageVO, ModuleVersionVO);

        	// Indexes that find Arrays of referencing VOs based on referenced VO plus fields in referencing VO
        	_index_chunkFileVOs_by_chunkVO_plus_languageId =
        		createIndex_AddDimensions(
        	        _index_chunkFileVOs_by_chunkVO,
        	        ["languageId"]);
        	_index_moduleVersionNativeLanguageVOs_by_moduleVersionVO_plus_languageId =
        		createIndex_AddDimensions(
        	        _index_moduleVersionNativeLanguageVOs_by_moduleVersionVO,
        	        ["languageId"]);
        	_index_moduleVersionTargetLanguageVOs_by_moduleVersionVO_plus_languageId =
        		createIndex_AddDimensions(
        	        _index_moduleVersionTargetLanguageVOs_by_moduleVersionVO,
        	        ["languageId"]);

        	Debug.info("Data.initIndexes() finished");
        }

        private function insertVOsIntoPrimaryStorageListsAndSetTheirIds(queryDataList:Array):void {
        	var queryData:SQLiteQueryData_Insert;
        	var storageList:ArrayCollection;
			for each (queryData in queryDataList) {
				storageList = getStorageListBasedOnVO(queryData.vo);
				// DataPrimaryStorageList.addItem() will set the VOs id
				storageList.addItem(queryData.vo);
			}
        }

        private function onInitPrimaryStorageListsFromSQLiteDBOperationComplete(event:Event):void {
            var operation:SequentialSQLiteTransactionSet = SequentialSQLiteTransactionSet(event.target);
            var queryData:SQLiteQueryData;
            var primaryStorageListName:String;
            var resultArray:Array;
            var o:Object;
            for (o in _index_primaryStorageListName_by_queryData) {
                queryData = SQLiteQueryData(o);
                primaryStorageListName = _index_primaryStorageListName_by_queryData[queryData];
                resultArray = operation.index_resultData_by_queryData[queryData];
                this[primaryStorageListName] = new ArrayCollection(resultArray);
            }
            initIndexes();
            _currentSQLiteOperation = null;
            operation.dispose();
            _callbacks.result(event);
            _callbacks = null;
        }

        private function onSQLiteDBDataModificationOperationResult(event:Event):void {
			Debug.info("Data.onSQLiteDBDataModificationOperationResult()");
            _currentSQLiteOperation = null;
            var operation:ISQLiteOperation = ISQLiteOperation(event.target);
            operation.dispose();
            _callbacks.result(event);
            _callbacks = null;
        }

        private function onSQLiteDBOperationError(event:BwErrorEvent):void {
			Debug.info("Data.onSQLiteDBOperationError()");
            _currentSQLiteOperation = null;
            var operation:ISQLiteOperation = ISQLiteOperation(event.target);
            operation.dispose();
            _callbacks.fault(event);
            _callbacks = null;
        }
    }
}